#include "CoolingBuffer.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <iostream>

namespace Slic3r {

std::string
CoolingBuffer::append(const std::string &gcode, std::string obj_id, size_t layer_id, float print_z)
{
    std::string out;
    if (this->_last_z.find(obj_id) != this->_last_z.end()) {
        // A layer was finished, Z of the object's layer changed. Process the layer.
        out = this->flush();
    }
    
    this->_layer_id = layer_id;
    this->_last_z[obj_id] = print_z;
    this->_gcode += gcode;
    // This is a very rough estimate of the print time, 
    // not taking into account the acceleration curves generated by the printer firmware.
    this->_elapsed_time          += this->_gcodegen->elapsed_time;
    this->_elapsed_time_bridges  += this->_gcodegen->elapsed_time_bridges;
    this->_elapsed_time_external += this->_gcodegen->elapsed_time_external;
    this->_gcodegen->elapsed_time          = 0;
    this->_gcodegen->elapsed_time_bridges  = 0;
    this->_gcodegen->elapsed_time_external = 0;
    
    return out;
}

void
apply_speed_factor(std::string &line, float speed_factor, float min_print_speed)
{
    // find pos of F
    size_t pos = line.find_first_of('F');
    size_t last_pos = line.find_first_of(' ', pos+1);
    
    // extract current speed
    float speed;
    {
        std::istringstream iss(line.substr(pos+1, last_pos));
        iss >> speed;
    }
    
    // change speed
    speed *= speed_factor;
    speed = std::max(speed, min_print_speed);
    
    // replace speed in string
    {
        std::ostringstream oss;
        oss.precision(3);
        oss << std::fixed << speed;
        line.replace(pos+1, (last_pos-pos), oss.str());
    }
}

std::string
CoolingBuffer::flush()
{
    GCode &gg = *this->_gcodegen;
    std::string gcode = this->_gcode;
    
    int fan_speed           = gg.config.fan_always_on ? gg.config.min_fan_speed.value : 0;
    float speed_factor      = 1.0;
    bool slowdown_external  = true;
    
    if (gg.config.cooling) {
        #ifdef SLIC3R_DEBUG
        printf("Layer %zu estimated printing time: %f seconds\n", this->_layer_id, this->_elapsed_time);
        #endif
        
        if (this->_elapsed_time < (float)gg.config.slowdown_below_layer_time) {
            // Layer time very short. Enable the fan to a full throttle and slow down the print
            // (stretch the layer print time to slowdown_below_layer_time).
            fan_speed = gg.config.max_fan_speed;
            
            // We are not altering speed of bridges.
            float time_to_stretch = this->_elapsed_time - this->_elapsed_time_bridges;
            float target_time = (float)gg.config.slowdown_below_layer_time - this->_elapsed_time_bridges;
            
            // If we spend most of our time on external perimeters include them in the slowdown,
            // otherwise only alter other extrusions.
            if (this->_elapsed_time_external < time_to_stretch/2.) {
                time_to_stretch -= this->_elapsed_time_external;
                target_time     -= this->_elapsed_time_external;
                slowdown_external = false;
            }
            
            speed_factor = time_to_stretch / target_time;
        } else if (this->_elapsed_time < (float)gg.config.fan_below_layer_time) {
            // Layer time quite short. Enable the fan proportionally according to the current layer time.
            fan_speed = gg.config.max_fan_speed
                - (gg.config.max_fan_speed - gg.config.min_fan_speed)
                * (this->_elapsed_time - (float)gg.config.slowdown_below_layer_time)
                / (gg.config.fan_below_layer_time - gg.config.slowdown_below_layer_time);
        }
        
        #ifdef SLIC3R_DEBUG
        printf("  fan = %d%%, speed = %f%%\n", fan_speed, speed_factor * 100);
        #endif
        
        if (speed_factor < 1.0) {
            // Adjust feed rate of G1 commands marked with an _EXTRUDE_SET_SPEED
            // as long as they are not _WIPE moves (they cannot if they are _EXTRUDE_SET_SPEED)
            // and they are not preceded directly by _BRIDGE_FAN_START (do not adjust bridging speed).
            std::string new_gcode;
            std::istringstream ss(gcode);
            std::string line;
            bool bridge_fan_start = false;
            while (std::getline(ss, line)) {
                if (boost::starts_with(line, "G1")
                    && boost::contains(line, ";_EXTRUDE_SET_SPEED")
                    && !boost::contains(line, ";_WIPE")
                    && !bridge_fan_start
                    && (slowdown_external || !boost::contains(line, ";_EXTERNAL_PERIMETER"))) {
                    apply_speed_factor(line, speed_factor, this->_min_print_speed);
                    boost::replace_first(line, ";_EXTRUDE_SET_SPEED", "");
                }
                bridge_fan_start = boost::starts_with(line, ";_BRIDGE_FAN_START");
                new_gcode += line + '\n';
            }
            gcode = new_gcode;
        }
    }
    if (this->_layer_id < gg.config.disable_fan_first_layers)
        fan_speed = 0;
    
    gcode = gg.writer.set_fan(fan_speed) + gcode;
    
    // bridge fan speed
    if (!gg.config.cooling || gg.config.bridge_fan_speed == 0 || this->_layer_id < gg.config.disable_fan_first_layers) {
        boost::replace_all(gcode, ";_BRIDGE_FAN_START", "");
        boost::replace_all(gcode, ";_BRIDGE_FAN_END", "");
    } else {
        boost::replace_all(gcode, ";_BRIDGE_FAN_START", gg.writer.set_fan(gg.config.bridge_fan_speed, true));
        boost::replace_all(gcode, ";_BRIDGE_FAN_END",   gg.writer.set_fan(fan_speed, true));
    }
    boost::replace_all(gcode, ";_WIPE", "");
    boost::replace_all(gcode, ";_EXTRUDE_SET_SPEED", "");
    boost::replace_all(gcode, ";_EXTERNAL_PERIMETER", "");
    
    // Reset the buffer.
    this->_elapsed_time          = 0;
    this->_elapsed_time_bridges  = 0;
    this->_elapsed_time_external = 0;
    this->_gcode = "";
    this->_last_z.clear(); // reset the whole table otherwise we would compute overlapping times
    
    return gcode;
}

}
